package com.clei.utils;

import com.clei.dto.file.FileDiff;
import com.clei.dto.file.FileName;
import com.clei.dto.file.FileNode;
import com.clei.dto.file.FileSize;
import com.clei.utils.container.Container;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 文件操作工具类
 *
 * @author KIyA
 */
public final class FileUtil {

    /**
     * 连接超时时间
     */
    private static final int CONNECTION_TIME_OUT = 3000;

    /**
     * 读取超时时间
     */
    private static final int READ_TIME_OUT = 3000;

    /**
     * 点符号
     */
    private static final char DOT = '.';

    /**
     * 文本文件处理默认编码
     */
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private FileUtil() {
    }

    /**
     * 重载一下 用路径可以代替file
     *
     * @param path          文件路径
     * @param fileOperation 文件操作
     */
    public static void fileOperation(String path, Consumer<File> fileOperation) {
        fileOperation(new File(path), fileOperation);
    }

    /**
     * 文件操作
     *
     * @param file          文件
     * @param fileOperation 文件操作
     */
    public static void fileOperation(File file, Consumer<File> fileOperation) {
        if (file.isDirectory()) {
            // 先序操作
            fileOperation.accept(file);
            File[] files = file.listFiles();
            if (null != files) {
                for (File f : files) {
                    fileOperation(f, fileOperation);
                }
            }
        } else {
            fileOperation.accept(file);
        }
    }

    /**
     * 只对第layer层的做处理
     *
     * @param file          file
     * @param layer         第几层
     * @param fileOperation 操作
     */
    public static void fileOperation(File file, Consumer<File> fileOperation, int layer) {
        if (0 > layer) {
            PrintUtil.log("参数错误 layer : {}", layer);
            return;
        }
        // 到指定层即结束
        if (0 == layer) {
            fileOperation.accept(file);
        } else {
            layer--;
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                if (null != files) {
                    for (File f : files) {
                        fileOperation(f, fileOperation, layer);
                    }
                }
            }
        }
    }

    /**
     * 打印文件树
     *
     * @param path 文件目录路径
     */
    public static void fileTree(String path) {
        fileTree(new File(path));
    }

    /**
     * 打印文件树
     *
     * @param file file
     */
    public static void fileTree(File file) {
        Container<Integer> counter = Container.of(0);
        // 文件数
        fileOperation(file, f -> counter.set(counter.get() + 1));
        int count = counter.get();
        if (1 == count) {
            PrintUtil.println(file.getAbsolutePath());
            return;
        }
        // 节点list
        List<FileNode> nodeList = new ArrayList<>(count);
        addToNodeList(file, nodeList);
        // 最大层
        Optional<Integer> maxOps = nodeList.stream().map(FileNode::getLayer).max(Comparator.comparingInt(i -> i));
        int maxLayer = maxOps.orElse(0);
        // 二维字符阵
        int y = maxLayer * 4;
        char[][] matrix = new char[count][y];
        List<String> resList = new ArrayList<>(count);
        resList.add(nodeList.get(0).getFile().getAbsolutePath());
        for (int i = 1; i < count; i++) {
            resList.add(null);
        }
        // 初始化
        for (int i = 1; i < count; i++) {
            FileNode node = nodeList.get(i);
            int spaceCount = (node.getLayer() - 1) * 4;
            int charCount = spaceCount + 4;
            char[] charArr = new char[charCount];
            matrix[i] = charArr;
            if (0 != spaceCount) {
                Arrays.fill(charArr, 0, spaceCount, ' ');
            }
            charArr[spaceCount] = '└';
            Arrays.fill(charArr, spaceCount + 1, charCount, '-');
        }
        // 倒序处理 中间连接线
        for (int i = count - 1; i != 0; i--) {
            FileNode node = nodeList.get(i);
            int layer = node.getLayer();
            if (layer > 0) {
                int end = node.getParentIndex();
                int tempY = (layer - 1) * 4;
                for (int j = i - 1; j > end; j--) {
                    if (' ' == matrix[j][tempY]) {
                        matrix[j][tempY] = '|';
                    }
                }
            }
            resList.set(i, new String(matrix[i]) + node.getFile().getName());
        }
        // 打印
        for (String s : resList) {
            PrintUtil.println(s);
        }
    }

    /**
     * 找到含有content且后缀名为fileSuffix的文件
     *
     * @param path       文件目录路径
     * @param fileSuffix 后缀名
     * @param content    查找内容
     */
    public static void findContent(String path, String fileSuffix, String content) {
        File file = new File(path);
        fileOperation(file, f -> {
            String fileName = f.getName();
            if (StringUtil.isNotEmpty(fileSuffix) && fileName.endsWith(fileSuffix)) {
                findContent(f, content);
            }
        });
    }

    /**
     * toBytes
     *
     * @param path filePath
     * @return byte[]
     */
    public static byte[] toBytes(String path) {
        return toBytes(new File(path));
    }

    /**
     * toBytes
     *
     * @param file file
     * @return byte[]
     */
    public static byte[] toBytes(File file) {
        byte[] bytes = null;
        try (FileInputStream fis = new FileInputStream(file);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            bytes = StreamUtil.toBytes(bis);
        } catch (Exception e) {
            PrintUtil.log("toBytes出错 path : {}", file.getPath(), e);
        }
        return bytes;
    }

    /**
     * toFile
     *
     * @param bytes byte[]
     * @param path  filePath
     * @return success true/false
     */
    public static boolean toFile(byte[] bytes, String path) {
        try (FileOutputStream fos = new FileOutputStream(path);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            bos.write(bytes);
            bos.flush();
            return true;
        } catch (Exception e) {
            PrintUtil.log("toFile error bytes : {}, path : {}", ByteUtil.toHex(bytes), path, e);
            return false;
        }
    }

    /**
     * toFile
     *
     * @param hex  hex
     * @param path filePath
     * @return success true/false
     */
    public static boolean toFile(String hex, String path) {
        byte[] bytes = ByteUtil.hex2Bytes(hex);
        boolean success = toFile(bytes, path);
        if (!success) {
            PrintUtil.log("toFile error hex : {}, path : {}", hex, path);
        }
        return success;
    }

    /**
     * toHex
     *
     * @param path filePath
     * @return hexString
     */
    public static String toHex(String path) {
        return toHex(new File(path));
    }

    /**
     * toHex
     *
     * @param file file
     * @return hexString
     */
    public static String toHex(File file) {
        String hex = null;
        try (FileInputStream fis = new FileInputStream(file);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            hex = StreamUtil.toHex(bis);
        } catch (Exception e) {
            PrintUtil.log("toHex出错 path : {}", file.getPath(), e);
        }
        return hex;
    }

    /**
     * printHex
     *
     * @param path filePath
     */
    public static void printHex(String path) {
        printHex(path, 8);
    }

    /**
     * printHex
     *
     * @param path          filePath
     * @param lineByteCount 每行多少个byte数据 一个byte两个字符
     */
    public static void printHex(String path, int lineByteCount) {
        printHex(new File(path), lineByteCount);
    }

    /**
     * printHex
     *
     * @param file file
     */
    public static void printHex(File file) {
        printHex(file, 8);
    }

    /**
     * printHex
     *
     * @param file          file
     * @param lineByteCount 每行多少个byte数据 一个byte两个字符
     */
    public static void printHex(File file, int lineByteCount) {
        bytesHandle(file, lineByteCount, (i, bytes) -> {
            int length = bytes.length;
            StringBuilder sb = new StringBuilder(length * 3 + 1);
            for (byte b : bytes) {
                sb.append(ByteUtil.toHex(b));
                sb.append(' ');
            }
            // 行号
            sb.append(i);
            PrintUtil.println(sb.toString());
        });
    }

    /**
     * 文本文件处理
     *
     * @param path        文本文件路径
     * @param lineHandler 行处理器
     */
    public static void txtHandle(String path, BiConsumer<Integer, String> lineHandler) {
        txtHandle(new File(path), lineHandler);
    }

    /**
     * 文本文件处理
     *
     * @param path        文本文件路径
     * @param lineHandler 行处理器
     * @param charset     charset
     */
    public static void txtHandle(String path, BiConsumer<Integer, String> lineHandler, Charset charset) {
        txtHandle(new File(path), lineHandler, charset);
    }

    /**
     * 文本文件处理
     *
     * @param file        文本文件
     * @param lineHandler 行处理器
     * @
     */
    public static void txtHandle(File file, BiConsumer<Integer, String> lineHandler) {
        txtHandle(file, lineHandler, DEFAULT_CHARSET);
    }

    /**
     * 文本文件处理
     *
     * @param file        文本文件
     * @param lineHandler 行处理器
     * @param charset     charset
     */
    public static void txtHandle(File file, BiConsumer<Integer, String> lineHandler, Charset charset) {
        try (FileInputStream fis = new FileInputStream(file);
             InputStreamReader isr = new InputStreamReader(fis, charset);
             BufferedReader br = new BufferedReader(isr)) {
            String str;
            int lineNumber = 0;
            while (null != (str = br.readLine())) {
                lineHandler.accept(++lineNumber, str);
            }
        } catch (Exception e) {
            PrintUtil.log("文本文件处理出错 path : {}", file.getPath(), e);
        }
    }

    /**
     * 文本文件处理
     *
     * @param path      文本文件路径
     * @param brHandler br处理器
     * @param <T>       T
     * @return T
     */
    public static <T> T txtHandle(String path, Function<BufferedReader, T> brHandler) {
        return txtHandle(new File(path), brHandler);
    }

    /**
     * 文本文件处理
     *
     * @param path      文本文件路径
     * @param brHandler br处理器
     * @param charset   charset
     * @param <T>       T
     * @return T
     */
    public static <T> T txtHandle(String path, Function<BufferedReader, T> brHandler, Charset charset) {
        return txtHandle(new File(path), brHandler, charset);
    }

    /**
     * 文本文件处理
     *
     * @param file      文本文件
     * @param brHandler br处理器
     * @param <T>       T
     * @return T
     */
    public static <T> T txtHandle(File file, Function<BufferedReader, T> brHandler) {
        return txtHandle(file, brHandler, DEFAULT_CHARSET);
    }

    /**
     * 文本文件处理
     *
     * @param file      文本文件
     * @param brHandler br处理器
     * @param charset   charset
     * @param <T>       T
     * @return T
     */
    public static <T> T txtHandle(File file, Function<BufferedReader, T> brHandler, Charset charset) {
        T res = null;
        try (FileInputStream fis = new FileInputStream(file);
             InputStreamReader isr = new InputStreamReader(fis, charset);
             BufferedReader br = new BufferedReader(isr)) {
            res = brHandler.apply(br);
        } catch (Exception e) {
            PrintUtil.log("文本文件处理出错 path : {}", file.getPath(), e);
        }
        return res;
    }

    /**
     * 文本文件过滤
     *
     * @param inputPath  输入文件
     * @param outputPath 输出文件
     * @param filter     过滤器
     * @param charset    charset
     */
    public static void txtFilter(String inputPath, String outputPath, Predicate<String> filter, Charset charset) {
        try {
            File inputFile = new File(inputPath);
            File outputFile = new File(outputPath);
            txtFilter(inputFile, outputFile, filter, charset);
        } catch (Exception e) {
            PrintUtil.log("文本文件过滤出错 input : {}, output : {}", inputPath, outputPath, e);
        }
    }

    /**
     * 文本文件过滤
     *
     * @param inputFile  输入文件
     * @param outputFile 输出文件
     * @param filter     过滤器
     * @param charset    charset
     */
    public static void txtFilter(File inputFile, File outputFile, Predicate<String> filter, Charset charset) {
        if (null == inputFile || null == outputFile) {
            return;
        }
        try (FileInputStream fis = new FileInputStream(inputFile);
             InputStreamReader isr = new InputStreamReader(fis, charset);
             BufferedReader br = new BufferedReader(isr);
             FileOutputStream fos = new FileOutputStream(outputFile);
             OutputStreamWriter osw = new OutputStreamWriter(fos, charset);
             BufferedWriter bw = new BufferedWriter(osw)) {
            // 转换写入
            int limitSize = 1000;
            List<String> strList = new ArrayList<>(limitSize);
            String str;
            while (null != (str = br.readLine())) {
                if (filter.test(str)) {
                    strList.add(str);
                    if (strList.size() == limitSize) {
                        for (String s : strList) {
                            bw.write(s);
                            bw.newLine();
                        }
                        strList.clear();
                        bw.flush();
                    }
                }
            }
            // 剩余
            if (!strList.isEmpty()) {
                for (String s : strList) {
                    bw.write(s);
                    bw.newLine();
                }
                bw.flush();
            }
        } catch (Exception e) {
            PrintUtil.log("文本文件过滤出错 input : {}, output : {}", inputFile.getPath(), outputFile.getPath(), e);
        }
    }

    /**
     * 二进制文件处理
     *
     * @param path         二进制文件路径
     * @param bytesHandler bytes处理器
     */
    public static void bytesHandle(String path, BiConsumer<Integer, byte[]> bytesHandler) {
        bytesHandle(path, 1024, bytesHandler);
    }

    /**
     * 二进制文件处理
     *
     * @param path         二进制文件路径
     * @param bufferSize   缓冲区大小
     * @param bytesHandler bytes处理器
     */
    public static void bytesHandle(String path, int bufferSize, BiConsumer<Integer, byte[]> bytesHandler) {
        bytesHandle(new File(path), bufferSize, bytesHandler);
    }

    /**
     * 二进制文件处理
     *
     * @param file         二进制文件
     * @param bytesHandler bytes处理器
     */
    public static void bytesHandle(File file, BiConsumer<Integer, byte[]> bytesHandler) {
        bytesHandle(file, 1024, bytesHandler);
    }

    /**
     * 二进制文件处理
     *
     * @param file         二进制文件
     * @param bufferSize   缓冲区大小
     * @param bytesHandler bytes处理器
     */
    public static void bytesHandle(File file, int bufferSize, BiConsumer<Integer, byte[]> bytesHandler) {
        try (FileInputStream fis = new FileInputStream(file);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            byte[] buffer = new byte[bufferSize];
            int length;
            int index = 0;
            while (-1 != (length = bis.read(buffer))) {
                if (length != bufferSize) {
                    bytesHandler.accept(index++, Arrays.copyOfRange(buffer, 0, length));
                } else {
                    bytesHandler.accept(index++, buffer);
                }
            }
        } catch (Exception e) {
            PrintUtil.log("二进制文件处理出错 path : {}", file.getPath(), e);
        }
    }

    /**
     * 二进制文件数据转换
     *
     * @param sourcePath 源文件
     * @param outputPath 输出文件
     * @param converter  转换器
     */
    public static void bytesConvert(String sourcePath, String outputPath, Function<byte[], byte[]> converter) {
        bytesConvert(new File(sourcePath), new File(outputPath), converter);
    }

    /**
     * 二进制文件数据转换
     *
     * @param sourceFile 源文件
     * @param outputFile 输出文件
     * @param converter  转换器
     */
    public static void bytesConvert(File sourceFile, File outputFile, Function<byte[], byte[]> converter) {
        try (FileInputStream fis = new FileInputStream(sourceFile);
             BufferedInputStream bis = new BufferedInputStream(fis);
             FileOutputStream fos = new FileOutputStream(outputFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            StreamUtil.bytesConvert(bis, bos, converter);
        } catch (Exception e) {
            PrintUtil.log("数据转换出错", e);
        }
    }

    /**
     * 将一个文本文件的全部内容读取到一个字符串内
     *
     * @param path 文件路径
     */
    public static String readTxt(String path) {
        return readTxt(new File(path));
    }

    /**
     * 将一个文本文件的全部内容读取到一个字符串内
     *
     * @param path    文件路径
     * @param charset charset
     */
    public static String readTxt(String path, Charset charset) {
        return readTxt(new File(path), charset);
    }

    /**
     * 将一个文本文件的全部内容读取到一个字符串内
     *
     * @param file 文本文件
     */
    public static String readTxt(File file) {
        return readTxt(file, DEFAULT_CHARSET);
    }

    /**
     * 将一个文本文件的全部内容读取到一个字符串内
     *
     * @param file    文本文件
     * @param charset charset
     */
    public static String readTxt(File file, Charset charset) {
        String separator = "\n";
        return txtHandle(file, br -> br.lines().collect(Collectors.joining(separator)), charset);
    }

    /**
     * 将一个文本文件的全部内容读取到一个list
     *
     * @param path 文件路径
     * @return list
     */
    public static List<String> readTxtInList(String path) {
        return readTxtInList(new File(path));
    }

    /**
     * 将一个文本文件的全部内容读取到一个list
     *
     * @param path    文件路径
     * @param charset charset
     * @return list
     */
    public static List<String> readTxtInList(String path, Charset charset) {
        return readTxtInList(new File(path), charset);
    }

    /**
     * 将一个文本文件的全部内容读取到一个list
     *
     * @param file 文本文件
     * @return list
     */
    public static List<String> readTxtInList(File file) {
        return readTxtInList(file, DEFAULT_CHARSET);
    }

    /**
     * 将一个文本文件的全部内容读取到一个list
     *
     * @param file    文本文件
     * @param charset charset
     * @return list
     */
    public static List<String> readTxtInList(File file, Charset charset) {
        return txtHandle(file, br -> br.lines().collect(Collectors.toList()), charset);
    }

    /**
     * 读取一个文本文件的行数
     *
     * @param path 文件路径
     * @return 文件行数
     */
    public static int txtLineCount(String path) {
        return txtLineCount(new File(path));
    }

    /**
     * 读取一个文本文件的行数
     *
     * @param path         文件路径
     * @param excludeBlank 排除空白行 true/false
     * @return 文件行数
     */
    public static int txtLineCount(String path, boolean excludeBlank) {
        return txtLineCount(new File(path), false);
    }

    /**
     * 读取一个文本文件的行数
     *
     * @param file 文本文件
     * @return 文件行数
     */
    public static int txtLineCount(File file) {
        return txtLineCount(file, false);
    }

    /**
     * 读取一个文本文件的行数
     *
     * @param file         文本文件
     * @param excludeBlank 排除空白行 true/false
     * @return 文件行数
     */
    public static int txtLineCount(File file, boolean excludeBlank) {
        Function<BufferedReader, Integer> brHandler = br -> {
            Stream<String> lines = br.lines();
            if (excludeBlank) {
                lines = lines.filter(StringUtil::isNotBlank);
            }
            return (int) lines.count();
        };
        return txtHandle(file, brHandler);
    }

    /**
     * 将一个String写入到指定文本文件里
     *
     * @param path 文件路径
     * @param str  字符串
     */
    public static void writeTxt(String path, String str) {
        writeTxt(path, Collections.singletonList(str));
    }

    /**
     * 将一个String写入到指定文本文件里
     *
     * @param path   文件路径
     * @param str    字符串
     * @param append 追加
     */
    public static void writeTxt(String path, String str, boolean append) {
        writeTxt(path, Collections.singletonList(str), append);
    }

    /**
     * 将一个String写入到指定文本文件里
     *
     * @param path    文件路径
     * @param str     字符串
     * @param charset charset
     */
    public static void writeTxt(String path, String str, Charset charset) {
        writeTxt(path, Collections.singletonList(str), charset);
    }

    /**
     * 将一个String写入到指定文本文件里
     *
     * @param path    文件路径
     * @param str     字符串
     * @param charset charset
     * @param append  追加
     */
    public static void writeTxt(String path, String str, Charset charset, boolean append) {
        writeTxt(path, Collections.singletonList(str), charset, append);
    }

    /**
     * 将一个StringList写入到指定文本文件里
     *
     * @param path     文件路径
     * @param lineList list
     */
    public static void writeTxt(String path, List<String> lineList) {
        writeTxt(path, lineList, DEFAULT_CHARSET, false);
    }

    /**
     * 将一个StringList写入到指定文本文件里
     *
     * @param path     文件路径
     * @param lineList list
     * @param append   追加
     */
    public static void writeTxt(String path, List<String> lineList, boolean append) {
        writeTxt(path, lineList, DEFAULT_CHARSET, append);
    }

    /**
     * 将一个StringList写入到指定文本文件里
     *
     * @param path     文件路径
     * @param lineList list
     * @param charset  charset
     */
    public static void writeTxt(String path, List<String> lineList, Charset charset) {
        writeTxt(path, lineList, charset, false);
    }

    /**
     * 将一个StringList写入到指定文本文件里
     *
     * @param path     文件路径
     * @param lineList list
     * @param charset  charset
     * @param append   追加 true是 false否
     */
    public static void writeTxt(String path, List<String> lineList, Charset charset, boolean append) {
        try (FileOutputStream fos = new FileOutputStream(path, append);
             OutputStreamWriter osw = new OutputStreamWriter(fos, charset);
             BufferedWriter bw = new BufferedWriter(osw)) {
            Iterator<String> iterator = lineList.iterator();
            // 追加的先来个换行
            if (append) {
                bw.newLine();
            }
            // 首行
            if (iterator.hasNext()) {
                String firstLine = iterator.next();
                bw.write(firstLine);
            }
            // 非首行
            while (iterator.hasNext()) {
                String str = iterator.next();
                if (null != str) {
                    bw.newLine();
                    bw.write(str);
                }
            }
            bw.flush();
        } catch (Exception e) {
            PrintUtil.log("文本写入出错 path : {}, lineList : {}", path, lineList, e);
        }
    }

    /**
     * 输出一个指定大小的二进制文件
     *
     * @param path 文件路径
     * @param size 文件大小
     */
    public static void writeBinFile(String path, long size) {
        int bufferSize = 8192;
        byte[] buffer = new byte[bufferSize];
        int writeTimes = (int) (size / bufferSize);
        int last = (int) (size % bufferSize);
        try (FileOutputStream fos = new FileOutputStream(path);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            for (int i = 0; i < writeTimes; i++) {
                bos.write(buffer);
            }
            if (last > 0) {
                bos.write(buffer, 0, last);
            }
        } catch (Exception e) {
            PrintUtil.log("文件写入出错 path : {}, size : {}, lineList : {}", path, size, e);
        }
    }

    /**
     * 获取某文件或文件夹大小
     *
     * @param path 文件路径
     */
    public static void getFileSize(String path) {
        getFileSize(path, null);
    }

    /**
     * 获取某文件或文件夹大小
     *
     * @param path 文件路径
     * @param unit 单位
     */
    public static void getFileSize(String path, String unit) {
        getFileSize(path, 0, unit);
    }

    /**
     * 获取某文件或文件夹下面的第n层文件/文件夹大小
     *
     * @param path  文件路径
     * @param layer 第几层
     * @param unit  单位
     */
    public static void getFileSize(String path, int layer, String unit) {
        File file = new File(path);
        List<FileSize> list = new ArrayList<>();
        fileOperation(file, f -> {
            String name = f.getName();
            String fullName = f.getAbsolutePath();
            long size = getFileSizeNoUnit(f);
            list.add(new FileSize(name, fullName, size));
        }, layer);
        // 这个超方便的，不用自己写comparable了。。
        // list = list.stream().sorted(Comparator.comparingLong(FileSize::getSize)).collect(Collectors.toList());
        list.sort(Comparator.comparing(FileSize::getSize).reversed());
        for (FileSize fs : list) {
            PrintUtil.log(fs.toString(unit));
        }
    }

    /**
     * 获得某文件或文件夹的大小
     *
     * @param file file
     * @return 文件大小
     */
    public static String getFileSize(File file) {
        long length = getFileSizeNoUnit(file);
        return getUnitString(length, null);
    }

    /**
     * 获得某文件或文件夹的大小
     * 没有单位
     *
     * @param file 文件
     * @return 文件大小
     */
    public static long getFileSizeNoUnit(File file) {
        long length = 0;
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (null != files) {
                for (File f : file.listFiles()) {
                    length += getFileSizeNoUnit(f);
                }
            }
        } else {
            length = file.length();
        }
        return length;
    }

    /**
     * 获取网络上文件大小
     *
     * @param url 文件http路径
     * @return 文件大小
     */
    public static String getHttpFileSize(String url) {
        return getHttpFileSize(url, null);
    }

    /**
     * 获取网络上文件大小
     *
     * @param url  文件url
     * @param unit 文件大小单位
     * @return 文件大小
     */
    public static String getHttpFileSize(String url, String unit) {
        long size = getHttpFileLength(url);
        return -1 == size ? "" : getUnitString(size, unit);
    }

    /**
     * 获取网络上文件大小
     *
     * @param url 文件url
     * @return 文件大小
     */
    public static long getHttpFileLength(String url) {
        try {
            URL httpUrl = new URL(url);
            URLConnection connection = httpUrl.openConnection();
            // 避免长时间等待
            connection.setConnectTimeout(CONNECTION_TIME_OUT);
            connection.setReadTimeout(READ_TIME_OUT);
            return connection.getContentLengthLong();
        } catch (Exception e) {
            PrintUtil.log("获取文件大小出错 url : {}", url, e);
            return -1;
        }
    }

    /**
     * 复制文件
     *
     * @param sourcePath 源文件路径
     * @param targetPath 输出文件路径
     */
    public static void copyFile(String sourcePath, String targetPath) {
        bytesConvert(sourcePath, targetPath, bytes -> bytes);
    }

    /**
     * 复制文件
     *
     * @param sourceFile 源文件
     * @param targetFile 目标文件
     */
    public static void copyFile(File sourceFile, File targetFile) {
        bytesConvert(sourceFile, targetFile, bytes -> bytes);
    }

    /**
     * 复制
     *
     * @param sourcePath 源文件路径
     * @param targetPath 输出文件路径
     */
    public static void cp(String sourcePath, String targetPath) {
        cp(new File(sourcePath), new File(targetPath));
    }

    /**
     * 复制
     *
     * @param sourceFile 源文件
     * @param targetPath 输出文件路径
     */
    public static void cp(File sourceFile, String targetPath) {
        cp(sourceFile, new File(targetPath));
    }

    /**
     * 复制
     *
     * @param sourceFile 源文件
     * @param targetFile 输出文件路径
     */
    public static void cp(File sourceFile, File targetFile) {
        if (!sourceFile.exists()) {
            throw new RuntimeException("源文件不存在");
        }
        if (sourceFile.isDirectory()) {
            File[] files = sourceFile.listFiles();
            if (null != files) {
                targetFile = new File(targetFile.getAbsolutePath() + File.separator + sourceFile.getName());
                targetFile.mkdirs();
            }
            for (File f : files) {
                cp(f, targetFile);
            }
        } else {
            if (targetFile.exists()) {
                // 复制文件到目标目录下
                if (targetFile.isDirectory()) {
                    copyFile(sourceFile, new File(targetFile.getAbsolutePath() + File.separator + sourceFile.getName()));
                } else {
                    // 复制且覆盖文件
                    copyFile(sourceFile, targetFile);
                }
            } else {
                // 姑且根据有无后缀名判断是文件还是文件夹
                int dotIndex = targetFile.getName().indexOf('.');
                if (-1 == dotIndex) {
                    targetFile.mkdirs();
                    targetFile = new File(targetFile.getAbsolutePath() + File.separator + sourceFile.getName());
                }
                copyFile(sourceFile, targetFile);
            }
        }
    }

    /**
     * 移动
     *
     * @param sourcePath 源文件路径
     * @param targetPath 输出文件路径
     * @return true/false
     */
    public static boolean mv(String sourcePath, String targetPath) {
        return mv(new File(sourcePath), new File(targetPath));
    }

    /**
     * 移动
     *
     * @param sourceFile 源文件
     * @param targetPath 输出文件路径
     * @return true/false
     */
    public static boolean mv(File sourceFile, String targetPath) {
        return mv(sourceFile, new File(targetPath));
    }

    /**
     * 移动
     *
     * @param sourceFile 源文件
     * @param targetFile 输出文件路径
     * @return true/false
     */
    public static boolean mv(File sourceFile, File targetFile) {
        if (!sourceFile.exists()) {
            throw new RuntimeException("源文件不存在");
        }
        File parentFile = targetFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        return sourceFile.renameTo(targetFile);
    }

    /**
     * 将大文件切割成多个指定大小的小文件
     *
     * @param sourceFilePath 源文件路径
     * @param outputPath     输出文件夹路径
     * @param outputFileName 输出文件名
     * @param limitSize      输出文件最大值限制 单位M
     * @throws Exception 各种异常
     */
    public static void cut(String sourceFilePath, String outputPath, String outputFileName, int limitSize) throws Exception {
        // 1M 对应limitSize的单位
        int bufferSize = 1024 * 1024;
        byte[] buffer = new byte[bufferSize];
        File inputFile = new File(sourceFilePath);

        // 创建输出目录
        File outputDirectoryFile = new File(outputPath);
        outputDirectoryFile.mkdirs();

        long fileSize = inputFile.length();
        // 输出文件数量
        long fileCount = fileSize / bufferSize / limitSize;
        if (fileSize % ((long) bufferSize * limitSize) > 0) {
            fileCount++;
        }

        // 输出文件的名称的最大长度
        int outputFileNameLength = (outputFileName + fileCount).length();

        // 切割
        try (FileInputStream fis = new FileInputStream(inputFile);
             BufferedInputStream bis = new BufferedInputStream(fis)) {

            int fileIndex = 0;
            // 输出文件
            String tempOutputFileName = StringUtil.complete(fileIndex + outputFileName, '0', outputFileNameLength);
            File outputFile = new File(outputPath + File.separator + tempOutputFileName);
            FileOutputStream fos = new FileOutputStream(outputFile);
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            int readTimes = 0;
            int length;
            while (-1 != (length = bis.read(buffer))) {
                readTimes++;

                if (readTimes == limitSize) {
                    readTimes = 1;
                    fileIndex++;

                    bos.flush();
                    bos.close();
                    fos.close();

                    tempOutputFileName = StringUtil.complete(fileIndex + outputFileName, '0', outputFileNameLength);
                    outputFile = new File(outputPath + File.separator + tempOutputFileName);
                    fos = new FileOutputStream(outputFile);
                    bos = new BufferedOutputStream(fos);
                }

                bos.write(buffer, 0, length);
            }

            bos.flush();
            bos.close();
            fos.close();
        }
    }


    /**
     * 将由大文件切割而成的多个的小文件
     * 合并成大文件
     *
     * @param sourceFilePath 源文件路径
     * @param outputFilePath 输出文件路径
     * @throws Exception 各种异常
     */
    public static void merge(String sourceFilePath, String outputFilePath) throws Exception {
        File outputFile = new File(outputFilePath);
        // 创建输出目录
        outputFile.getParentFile().mkdirs();
        FileOutputStream fos = new FileOutputStream(outputFile);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 1M
        int bufferSize = 1024 * 1024;
        byte[] buffer = new byte[bufferSize];
        File inputFile = new File(sourceFilePath);
        File[] files = inputFile.listFiles();
        // 合并
        for (File file : files) {
            try (FileInputStream fis = new FileInputStream(file);
                 BufferedInputStream bis = new BufferedInputStream(fis)) {

                int length;
                while (-1 != (length = bis.read(buffer))) {
                    bos.write(buffer, 0, length);

                }
            }
        }
        bos.flush();
        bos.close();
        fos.close();
    }

    /**
     * 简单的文件加密
     *
     * @param sourcePath 加密文件路径
     * @param outputPath 输出文件路径
     * @param delta      增量
     */
    public static void encrypt(String sourcePath, String outputPath, byte delta) {
        Function<byte[], byte[]> converter = bytes -> {
            byte[] data = new byte[bytes.length];
            for (int i = 0; i < data.length; i++) {
                data[i] = (byte) (bytes[i] + delta);
            }
            return data;
        };
        encrypt(sourcePath, outputPath, converter);
    }

    /**
     * 简单的文件加密
     *
     * @param sourcePath 加密文件路径
     * @param outputPath 输出文件路径
     */
    public static void encrypt(String sourcePath, String outputPath) {
        Function<byte[], byte[]> converter = bytes -> {
            byte[] data = new byte[bytes.length];
            for (int i = 0; i < data.length; i++) {
                data[i] = (byte) -bytes[i];
            }
            return data;
        };
        encrypt(sourcePath, outputPath, converter);
    }

    /**
     * 简单的文件加密
     *
     * @param sourcePath 加密文件路径
     * @param outputPath 输出文件路径
     * @param converter  转换器
     */
    public static void encrypt(String sourcePath, String outputPath, Function<byte[], byte[]> converter) {
        bytesConvert(sourcePath, outputPath, converter);
    }

    /**
     * 比较两个目录下的文件差异
     *
     * @param path1     path1
     * @param path2     path2
     * @param suffixArr suffixArr 例：["json","txt","java"]
     * @return 差异数据list
     */
    public static List<FileDiff> txtDiff(String path1, String path2, String[] suffixArr) {
        return txtDiff(new File(path1), new File(path2), suffixArr);
    }

    /**
     * 比较两个目录下的文件差异
     *
     * @param directory1 目录1
     * @param directory2 目录2
     * @param suffixArr  suffixArr 例：["json","txt","java"]
     * @return 差异数据list
     */
    public static List<FileDiff> txtDiff(File directory1, File directory2, String[] suffixArr) {
        // 结果list
        List<FileDiff> diffList = new ArrayList<>();
        // 后缀集
        Set<String> suffixSet = Arrays.stream(suffixArr).collect(Collectors.toSet());
        File[] files1 = directory1.listFiles();
        File[] files2 = directory2.listFiles();
        Map<String, File> map1 = Arrays.stream(files1).collect(Collectors.toMap(File::getName, f -> f));
        Map<String, File> map2 = Arrays.stream(files2).collect(Collectors.toMap(File::getName, f -> f));
        Set<String> fileNameSet = new HashSet<>(map1.keySet());
        fileNameSet.addAll(map2.keySet());
        // 排序
        List<String> fileNameList = new ArrayList<>(fileNameSet);
        Collections.sort(fileNameList);
        // 处理
        for (String fileName : fileNameList) {
            File f1 = map1.get(fileName);
            File f2 = map2.get(fileName);
            if (null != f1 && null != f2 && f1.exists() && f2.exists()) {
                if (f1.isDirectory() && f2.isDirectory()) {
                    diffList.addAll(txtDiff(f1, f2, suffixArr));
                } else if (!f1.isDirectory() && !f2.isDirectory()) {
                    if (fileNameSuffixInSet(fileName, suffixSet)) {
                        FileDiff fileDiff = txtDiff(f1, f2);
                        if (null != fileDiff) {
                            diffList.add(fileDiff);
                        }
                    }
                } else {
                    diffList.add(new FileDiff(f1.getPath(), f2.getPath(), 3, null));
                }
            } else {
                boolean f1Exists = null != f1 && f1.exists();
                File existFile = f1Exists ? f1 : f2;
                // 文件不存在的只判断目录类型的和bin类型且在后缀集里的
                if (existFile.isDirectory() || fileNameSuffixInSet(fileName, suffixSet)) {
                    String fileName1 = directory1.getPath() + File.separator + fileName;
                    String fileName2 = directory2.getPath() + File.separator + fileName;
                    if (f1Exists) {
                        fileName1 = fileName1 + "不存在";
                    } else {
                        fileName2 = fileName2 + "不存在";
                    }
                    diffList.add(new FileDiff(fileName1, fileName2, 1, null));
                }
            }
        }
        return diffList;
    }

    /**
     * 获取文件名后缀
     *
     * @param fileName 文件名
     * @return 后缀
     */
    public static String getSuffix(String fileName) {
        int dotIndex = fileName.lastIndexOf(DOT);
        return -1 == dotIndex ? "" : fileName.substring(dotIndex + 1);
    }

    /**
     * 获取文件名信息
     *
     * @param filePath 文件全路径
     * @return 后缀
     */
    public static FileName getFileName(String filePath) {
        File file = new File(filePath);
        String name = file.getName();
        int dotIndex = name.lastIndexOf(DOT);
        String simpleName = "";
        String suffix = "";
        if (-1 != dotIndex) {
            simpleName = name.substring(0, dotIndex);
            suffix = name.substring(dotIndex + 1);
        }
        return new FileName(name, simpleName, suffix, file.getParent(), filePath);
    }

    /**
     * 比较两个文本文件的差异
     * 粗略地按照行号比较，需要更精确的可以使用BeyondCompare git之类的来比
     *
     * @param file1 目录1
     * @param file2 目录2
     * @return FileDiff
     */
    public static FileDiff txtDiff(File file1, File file2) {
        try (FileReader fr1 = new FileReader(file1);
             BufferedReader br1 = new BufferedReader(fr1);
             FileReader fr2 = new FileReader(file2);
             BufferedReader br2 = new BufferedReader(fr2)) {
            String line1 = br1.readLine();
            String line2 = br2.readLine();
            int lineNumber = 0;
            // 连续差异
            int diffStart = -1;
            int diffEnd = -1;
            String diffTemp = null;
            // 差异内容
            StringBuilder diffSb = new StringBuilder();
            while (null != line1 || null != line2) {
                lineNumber++;
                if (!Objects.equals(line1, line2)) {
                    // 连续差异延续
                    if (1 == lineNumber - diffEnd) {
                        diffEnd++;
                    } else {
                        if (diffEnd != diffStart) {
                            diffSb.append(diffStart).append('-').append(diffEnd).append("连续差异").append('\n');
                        } else if (null != diffTemp) {
                            diffSb.append(diffTemp);
                        }
                        diffStart = lineNumber;
                        diffEnd = lineNumber;
                        diffTemp = lineNumber + "||" + line1 + "||" + line2 + '\n';
                    }
                }
                if (null != line1) {
                    line1 = br1.readLine();
                }
                if (null != line2) {
                    line2 = br2.readLine();
                }
            }
            if (diffEnd != diffStart) {
                diffSb.append(diffStart).append('-').append(diffEnd).append("连续差异").append('\n');
            } else if (null != diffTemp) {
                diffSb.append(diffTemp);
            }
            return 0 == diffSb.length() ? null : new FileDiff(file1.getPath(), file2.getPath(), 2, diffSb.toString());
        } catch (Exception e) {
            PrintUtil.log("文本文件比较出错 ", e);
        }
        return null;
    }

    /**
     * delete
     *
     * @param file file
     */
    private static void delete(File file) {
        if (!file.exists()) {
            return;
        }
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (null != files) {
                for (File f : files) {
                    delete(f);
                }
            }
        }
        file.delete();
    }

    /**
     * 找到含有content内容的文件
     *
     * @param file    文本文件
     * @param content 查找内容
     */
    public static void findContent(File file, String content) {
        Container<Boolean> container = Container.of(Boolean.FALSE);
        List<String> matchList = new ArrayList<>();
        txtHandle(file, (lineNumber, str) -> {
            if (str.contains(content)) {
                container.set(Boolean.TRUE);
                matchList.add(lineNumber + "  " + str);
            }
        });
        if (container.get()) {
            PrintUtil.log(file.getPath());
            matchList.forEach(PrintUtil::log);
        }
    }

    /**
     * 根据文件大小返回大小
     * 例1：1024,k -> 1.00K
     * 例2：1024,null -> 1024B
     *
     * @param length 大小
     * @param unit   单位
     * @return byte单位转换
     */
    public static String getUnitString(long length, String unit) {
        // 默认单位为B 字节
        if (StringUtil.isEmpty(unit)) {
            unit = "B";
        }
        // 除数
        int divisor;
        switch (unit) {
            case "G":
                divisor = 1024 * 1024 * 1024;
                break;
            case "M":
                divisor = 1024 * 1024;
                break;
            case "K":
                divisor = 1024;
                break;
            default:
                return length + "B";

        }
        // 保留两位小数
        BigDecimal result = new BigDecimal(length).divide(new BigDecimal(divisor), 2, RoundingMode.HALF_UP);
        return result + unit;
    }

    /**
     * 文件后缀名是否在集合内
     *
     * @param fileName  文件名
     * @param suffixSet 后缀集
     * @return boolean
     */
    private static boolean fileNameSuffixInSet(String fileName, Set<String> suffixSet) {
        // 先判断文件名是abc.txt还是abcd这种
        int dotIndex = fileName.indexOf('.');
        if (-1 != dotIndex) {
            return suffixSet.contains(fileName.substring(dotIndex + 1));
        } else {
            for (String suffix : suffixSet) {
                if (fileName.endsWith(suffix)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * 添加到nodeList
     *
     * @param file     file
     * @param nodeList nodeList
     */
    private static void addToNodeList(File file, List<FileNode> nodeList) {
        addToNodeList(file, -1, 0, nodeList);
    }

    /**
     * 添加到nodeList
     *
     * @param file     file
     * @param nodeList nodeList
     */
    private static void addToNodeList(File file, int parentIndex, int layer, List<FileNode> nodeList) {
        FileNode node = new FileNode(file, parentIndex, layer);
        nodeList.add(node);
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            parentIndex = nodeList.size() - 1;
            layer++;
            if (null != files) {
                for (File f : files) {
                    addToNodeList(f, parentIndex, layer, nodeList);
                }
            }
        }
    }
}
